본문으로 건너뛰기

Part 3: API Security Design

ItemContent
Document NamePart 3: API Security Design
Product NameDTA Wide Sleep Management Platform
Date2026-02-10
ScopePart 3 (Backend API)

1. API Security Overview

Objective: Comply with OWASP API Top 10, secure RESTful API design

Security Principles:

  • Authentication/authorization mandatory (JWT Bearer Token)
  • Input validation (DTO + class-validator)
  • Rate Limiting (Redis-based)
  • Minimal error message information disclosure
  • Additional protection for admin endpoints

2. Authentication and Authorization

2.1 JWT-Based Authentication

AppToken Structure (RS256 Asymmetric Signing):

// AppToken Payload (Actual Implementation - app-token.guard.ts)
{
"appId": "app-uuid",
"deviceId": "device-uuid",
"jti": "token-unique-id",
"exp": 1707579000, // Expire after 30 minutes
"iat": 1707577200
}

Authentication Flow:

Token Revocation Policy:

Inactivity Detection Implementation Status:

ItemNotes
AccessToken TTL 30-minute expirationBased on JWT exp claim
RefreshToken TTL 14-day expirationBased on JWT exp claim, long-term inactivity

2.2 Role-Based Authorization (RBAC)

Decorator Usage:

@Controller('sleep')
@UseGuards(AppTokenGuard, FlexibleAuthGuard)
export class SleepController {
@Get('/logs')
@Roles('patient', 'clinician', 'admin')
async getSleepLogs(@CurrentUser() user: TokenPayload) {
// Patient: own data only
// Clinician: assigned patients only
// Admin: all data
}

@Delete('/logs/:id')
@Roles('patient', 'admin')
async deleteSleepLog(@Param('id') id: string) {
// Patient: delete own data only
// Admin: delete all data
}
}

2.3 App Request Integrity and Authenticity Verification

Currently Implemented Validation Layers:

Validation LayerImplementation StatusReference FileNotes
AppToken (RS256) App Authenticity Verification✅ Implementedapp-token.guard.ts, app-token.service.tsJWK-based RS256 signing + DB status check
Device ID SHA-256 Hash Verification✅ Implementeddevice-identifier.service.tsRequest deviceIdHash vs server calculated value comparison
PubSub/Webhook GCP OIDC JWT Verification✅ Implementedpubsub.guard.ts, webhook-validation.service.tsGCP Pub/Sub request authentication
HMAC-based Request Body Signature Verification❌ Not implemented-No additional signature beyond AppToken
Response Body HMAC/Signature Addition❌ Not implemented-No signature layer on responses

3. Input Validation

3.1 DTO-Based Validation

CreateSleepLogDto:

import { IsString, IsDateString, IsOptional, IsInt, Min, Max } from 'class-validator';

export class CreateSleepLogDto {
@IsDateString()
bedtime: string; // ISO 8601 format

@IsDateString()
wakeTime: string;

@IsOptional()
@IsInt()
@Min(0)
@Max(480) // Maximum 8 hours
sol?: number; // Sleep Onset Latency (minutes)

@IsOptional()
@IsString()
@MaxLength(500)
notes?: string;
}

ValidationPipe Configuration:

// main.ts
app.useGlobalPipes(
new ValidationPipe({
transform: true, // Auto type conversion
whitelist: true, // Remove non-DTO properties
forbidNonWhitelisted: true, // Error on extra properties
exceptionFactory: (errors) => {
// Custom error response
return new BadRequestException({
code: 'VALIDATION_ERROR',
message: 'Input validation failed',
details: errors.map(e => ({
property: e.property,
constraints: e.constraints
}))
});
}
})
);

3.2 SQL Injection Prevention

Prisma ORM Parameterization:

// ✅ SAFE: Prisma parameterized query
async findByUserId(userId: string): Promise<SleepLog[]> {
return this.prisma.sleepLog.findMany({
where: { userId: userId } // Auto parameterized
});
}

3.3 XSS Prevention

HTML Escaping:

import { sanitize } from 'class-sanitizer';

export class CreateNoteDto {
@IsString()
@MaxLength(1000)
@sanitize() // Remove HTML tags
content: string;
}

// Or manual escaping
function syntax(text: string): string {
return text
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#x27;');
}

4. Rate Limiting and Anti-Abuse

4.1 Rate Limiting Policy

Endpoint TypeLimitWindowOn Exceeded
Authentication (Login)5 req/minutePer user429 + 1 min wait
Data Retrieval (GET)100 req/minutePer user429 + retry guidance
Data Creation (POST)10 req/minutePer user429 + retry guidance
Admin API60 req/minutePer user429 + logging
Public API1000 req/minutePer IP429 + CAPTCHA

4.2 Redis-Based Rate Limiter

Implementation (NestJS Interceptor):

@Injectable()
export class RateLimitInterceptor implements NestInterceptor {
constructor(private readonly redis: RedisService) {}

async intercept(context: ExecutionContext, next: CallHandler): Promise<Observable<any>> {
const request = context.switchToHttp().getRequest();
const userId = request.user?.userId || request.ip;
const key = `rate-limit:${request.path}:${userId}`;

const current = await this.redis.incr(key);
if (current === 1) {
await this.redis.expire(key, 60); // 1 min TTL
}

const limit = this.getLimit(request.path);
if (current > limit) {
throw new TooManyRequestsException(`Rate limit exceeded. Try again in 60 seconds.`);
}

return next.handle();
}
}

4.3 Brute Force Protection

Login Failure Limit:

async login(email: string, password: string) {
const key = `login-attempts:${email}`;
const attempts = await this.redis.get(key);

if (attempts && parseInt(attempts) >= 5) {
throw new TooManyRequestsException('Too many login attempts. Try again in 15 minutes.');
}

// Password verification
const isValid = await this.verifyPassword(email, password);

if (!isValid) {
await this.redis.incr(key);
await this.redis.expire(key, 900); // 15 minutes
throw new UnauthorizedException('Invalid credentials');
}

// Reset counter on success
await this.redis.del(key);
return this.generateToken(user);
}

5. Error Message Policy

5.1 Safe Error Response

Production Environment:

// ✅ SAFE: Minimal information disclosure
{
"code": 2051,
"message": "INVALID_TOKEN",
"detail": "The token is invalid.",
"timestamp": "2026-02-10T12:00:00.000Z"
}

Global Exception Filter:

@Catch()
export class GlobalExceptionFilter implements ExceptionFilter {
catch(exception: unknown, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse();
const request = ctx.getRequest();

let status = 500;
let code = 'INTERNAL_SERVER_ERROR';
let detail = 'A server error occurred.';

if (exception instanceof HttpException) {
status = exception.getStatus();
const exceptionResponse = exception.getResponse();
code = exceptionResponse['code'] || exception.message;
detail = exceptionResponse['detail'] || exception.message;
}

// ❌ Absolutely never expose stack traces
// ❌ Absolutely never expose database connection info
// ❌ Absolutely never expose file paths

response.status(status).json({
code,
message: code,
detail,
timestamp: new Date().toISOString()
});

// Internal logging (Cloud Logging / LoggerService)
if (status >= 500) {
this.logger.error(exception);
}
}
}
ItemImplementation StatusNotes
Error message minimization (no stack trace exposure)✅ ImplementedGlobalExceptionFilter

5.2 HTTP Status Code Mapping

ScenarioHTTP StatusResponse CodeMessage
Authentication failed (no token)401AUTH_REQUIRED"Authentication required."
Authentication failed (token expired)401TOKEN_EXPIRED"Token has expired."
Permission denied403FORBIDDEN"Permission denied."
Resource not found404NOT_FOUND"Resource not found."
Input validation failed400VALIDATION_ERROR"Input value is invalid."
Rate limit exceeded429RATE_LIMIT_EXCEEDED"Request limit exceeded."
Server error500INTERNAL_ERROR"A server error occurred."

6. Admin Endpoint Protection

6.1 Additional Security Layer

Admin-only Guard:

@Injectable()
export class AdminGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest();
const user = request.user;

// 1. Check Admin role
if (!user.roles?.includes('admin')) {
return false;
}

// 2. IP whitelist check (optional)
const clientIp = request.headers['x-forwarded-for'] || request.connection.remoteAddress;
if (!this.isAllowedIp(clientIp)) {
this.logger.warn(`Admin access from unauthorized IP: ${clientIp}`);
return false;
}

// 3. Time-based access restriction (optional)
const hour = new Date().getUTCHours();
if (hour >= 0 && hour < 6) {
this.logger.warn(`Admin access during restricted hours: ${user.userId}`);
// Warn only, don't block
}

return true;
}
}

6.2 Sensitive API Endpoint List

EndpointRoleAdditional SecurityAudit Log
DELETE /v1/admin/users/:idAdminMFA reconfirmation
POST /v1/admin/users/:id/rolesAdminJira approval
GET /v1/admin/audit-logsAdminIP whitelist
POST /v1/admin/system/settingsAdminJira approval
GET /v1/admin/users/:id/data-exportAdmin, CSData anonymization

7. API Security Checklist

#ItemImplementationValidation
1All endpoints require authentication (except public)API Test
2Role-based authorization (RBAC)API Test
3DTO input validation (class-validator)API Test
4SQL Injection prevention (Prisma ORM)SAST
5XSS prevention (HTML escaping)DAST
6CSRF prevention (SameSite Cookie)N/AJWT Bearer Token used
7Rate Limiting (Redis)API Test
8Safe error messagesAPI Test
9HTTPS enforcement (TLS 1.3)GCP SSL
10Sensitive data logging prohibitionLog sample validation
11CORS appropriate configurationConfiguration review
12API versioning (v1/)URL pattern
13Admin API additional protectionAdmin Guard
14Audit logging (sensitive actions)Log sample

8. OWASP API Top 10 Compliance

#OWASP API Top 10Implementation ControlStatus
1Broken Object Level AuthorizationuserId validation, RBAC
2Broken AuthenticationJWT, Rate Limiting
3Broken Object Property Level AuthorizationDTO Whitelist
4Unrestricted Resource ConsumptionRate Limiting, pagination
5Broken Function Level AuthorizationRoles Guard
6Unrestricted Access to Sensitive Business FlowsRate Limiting, block access
7Server Side Request Forgery (SSRF)URL validation, whitelist
8Security MisconfigurationSecure defaults, SAST
9Improper Inventory ManagementSwagger docs, API versioning
10Unsafe Consumption of APIsExternal API TLS validation, timeout

9. Swagger API Documentation Security

9.1 Swagger UI Access Restriction

Production Environment:

// main.ts
if (process.env.NODE_ENV !== 'production') {
// Enable Swagger only in staging/development
const config = new DocumentBuilder()
.setTitle('DTA Wide API')
.setVersion('1.0')
.addBearerAuth()
.build();

const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('v1/docs', app, document);
} else {
// Production: Disable Swagger or IP whitelist
// [TODO: Production Swagger disabling verification needed]
}

9.2 Hide Sensitive Information

@ApiProperty({
example: 'user@example.com',
description: 'User email'
// ❌ Never include: actual emails, tokens, passwords
})
email: string;

@ApiProperty({
example: '********', // Masked example
writeOnly: true, // Exclude from response
})
@Exclude() // Auto-exclude with class-transformer
password: string;

Evidence and References (Artifacts)

  1. API Security Checklist (Section 7 of this document)
  2. DTO Validation Code - apps/dta-wide-api/src/app/*/dto/*.dto.ts
  3. Guards Implementation - guards/app-token.guard.ts, guards/flexible-auth.guard.ts, guards/webhook.guard.ts
  4. Rate Limiter Code - libs/core/redis/src/lib/rate-limit.interceptor.ts
  5. Exception Filter Code - apps/dta-wide-api/src/app/filters/global-exception.filter.ts
  6. OWASP ZAP DAST Report - reports/api-dast-latest.pdf
  7. Swagger API Documentation - https://staging-api.dta-wide.com/v1/docs (staging)
  8. API Security Test Results - test-results/api-security-tests.log
  9. Rate Limiting Test - k6 script + results
  10. Audit Log Sample (admin API) - logs/admin-api-calls.log
Test ItemToolResultNotes
SQL InjectionOWASP ZAP[TODO: Execution needed]Prisma ORM (parameterized)
XSS (Reflected)OWASP ZAP[TODO: Execution needed]HTML escaping
Broken AuthenticationBurp Suite[TODO: Execution needed]AppToken RS256 + Rate Limit
IDOR (Insecure Direct Object Reference)Manual Test[TODO: Execution needed]userId validation
Rate Limitingk6 Load Test[TODO: Execution needed]100 req/min per user
CSRFOWASP ZAPN/AJWT Bearer Token (no cookies)
Sensitive Information DisclosureManual Review[TODO: Execution needed]Error message minimization